package model;

import java.util.ArrayList;

import io.AudioPlay;
import io.Load;
import io.Save;

import org.bushe.swing.event.EventBus;
import org.bushe.swing.event.annotation.*;

import constants.Rules;
import constants.Constants;
import constants.Events;
import control.Main;

import model.i.IModel;
import model.i.IRules;
import model.i.IState;
import model.playingfield.PlayingField;

/**
 * @author christopher, modified by Mruzuki
 * This the Model Class. It acts as a central control module for
 * the Model part of our design, and hence handles interaction with
 * all underlying subsystems on this part of the abstraction.
 *
 * The Model also contains core logic related to the execution of the game.
 * Most importantly
 * It handles all interactions with the View.
 */

public class Model implements IModel {

    /**
     * The Die, which will be used to determine the behavior of each players turn
     */
    private Die die = new Die();

    /**
     * The list of players currently in the game, indexed from 1 to 4
     */
    private Player[] players;;

    /**
     * The High-Score
     */

    /**
     * The Rules for this session of the game. The Rules will determine how the
     * game will respond to core events during the game.
     */
    private IRules rules;

    /**
     * The State for this session of the game. The State will store:
     * 	 - which players are in the game
     *   - various state variables regarding the game.
     */
    private IState state = new State();

    /**
     * The Playingfield for this session of the game. The PlayingField stores
     * the location of pieces, and also provide an interface for moving them
     * around.
     */
    private PlayingField playingfield;

    /**
     * The Statistics for the game in progress.
     */
    private Statistics statistics;

    /**
     * The audio in the game
     */
    private AudioPlay audio = new AudioPlay();

    /**
     * The timer in the game
     */
    private TurnTimer timer;

    /**
     * The following are state variables, used in order to determine what phase
     * of a players turn the Model currently is in, and data related to it:
     *   - playerSelected : a player is currently selected
     *   - dieRolled      : the die has been rolled
     *   - spotSelected   : a piece has been selected
     *   - moveMade 	  : the move has been made
     *
     *   - currentPlayer  : the Player which the current turn belongs to
     */

    public Model() {
        AnnotationProcessor.process(this);
    }

    @Override
    public void setState(IState state) {

        this.state = state;
    }
    @Override
    public IState getState() {

        return state;
    }

    /**
     * The following functions are eventhandlers, and specify
     * how the program should react to various events coming over
     * the EventBus.
     *
     * This is a crucial stage of functionality: the program should react
     * to events in accordance with the values of it's state variables - it
     * should for example not accept a new dicethrow if one has already been
     * done.
     */

    /**
     * This is the first core event, and defines what happens when a DICETHROW
     * event is broadcasted on the bus. The Model should simply catch the event,
     * and consult the Rules class about what it is supposed to do.
     */

    @EventTopicSubscriber(topic="DICETHROW")
    public void dicethrowEvent(String topic, Integer value) {
        System.out.println("Model received event " + topic);

        value = die.throwDie();

        /*
         * Is there no game in progress? In that case, throw an error and
         * return.
         */
        if(!state.isGameInProgress()) {
            EventBus.publish(Events.ERROR.toString(), "There is no game in progress!");
            return;
        }


        /*
         * Calculate a new die value, and fire an event to make View simulate the way
         * we acquired it for the user.
         */
        EventBus.publish(Events.DICETHROWUPDATE.toString(), value);
        audio.playSound(Constants.Sounds.diethrow);
        /*
         * Finally, execute the Rules logic, tell View to update, and
         * move forward.
         */
        rules.dieRolled(value);
        EventBus.publish(Events.UPDATE.toString(), this);
    }

    /**
     * This is the second core event - after the dice is rolled, the Model will be
     * idle until the player chooses to click somewhere on the playing field. The
     * Model will then catch an event related to this, and consult rules on what
     * to do with it.
     *
     * After this, the Model will call the cleanUp() method from Rules in order to
     * perform last steps before passing the game on to the next player.
     * @param topic
     * @param data
     */
    @EventTopicSubscriber(topic="TIME")
    public void onTimerTick(String topic, Integer turnTimeLeft){
        if(turnTimeLeft == 0){
            System.out.println("Another print");
            rules.onTimeOut();
            this.newTurn();
            EventBus.publish(Events.UPDATE.toString(), this);
        }
    }

    @EventTopicSubscriber(topic="END_GAME")
    public void onEndGame(String topic, Object unused){
        if(state.isGameInProgress()){
            state.resetStateVars();
            System.out.println("3");
            timer.stop();
            System.out.println("4");
        }
        System.out.println("1");
        Main.s.save();
        System.out.println("2");
    }

    @EventTopicSubscriber(topic="CHANGE_RULES")
    public void onChangeRules(String topic, ArrayList<Rules> r ){
        if(!state.isGameInProgress())
            return;
        for(Rules rule:r){
            if(rule.equals(Rules.ENTEREASY)){
                rules.setEnterGoalEasy(!rules.isEnterGoalEasyOn());
                System.out.println("toggle entereasy");
            }
            if(rule.equals(Rules.PUSH)){
                rules.setPushAllowed(!rules.isPushAllowedOn());
                System.out.println("toggle push");
            }
        }
    }

    @EventTopicSubscriber(topic="PLAYERNAME")
    public void onPlayerNameChange(String topic, String[] players){

        if(!state.isGameInProgress()){
            EventBus.publish(Events.ERROR.toString(), "Start the game first!");
            return;
        }

        for(int i = 0; i < 4;){
            if(players[i].compareTo("Default")==0){
                i++;
            }
            else{
                state.getPlayer(i).setPlayerName(players[i]);
                i++;
            }
        }
        EventBus.publish(Events.UPDATE.toString(), this);
    }

    @EventTopicSubscriber(topic="SFX")
    public void onSFX(String topic, Boolean onOFF){
        System.out.println("From model: SFX is ON ");
        audio.setEnabled(onOFF);
    }

    @EventTopicSubscriber(topic="MOVE")
    public void selectSpotEvent(String topic, Integer location) {
        System.out.println("Model received event " + topic);

        /*
         * Is there no game in progress? In that case, throw an error and
         * return.
         */
        if(!state.isGameInProgress()) {
            EventBus.publish(Events.ERROR.toString(), "There is no game in progress!");
            return;
        }

        rules.movePiece(location, getState().getCurrentDieValue());
        //rules.cleanUp();

        if(state.getBattle() == null)
            EventBus.publish(Events.UPDATE.toString(), this);


    }

    /**
     * This
     * @param topic
     * @param data
     */
    @EventTopicSubscriber(topic="SELECT_PLAYER")
    public void selectPlayerEvent(String topic, Object data) {
        System.out.println("Model received event " + topic);

    }

    @Override
    public void setRules(IRules rules) {
        // TODO Auto-generated method stub

    }

    /**
     * This handles the NEWGAME event, which essentially causes the Model to wipe its
     * slate and start a completely new game.
     *
     * This event will normally be thrown when the user clicks the "Start New Game" option from
     * the menu. The user will be prompted by the system to enter some basic info, and this data
     * will then be sent on to the Model, which will use it in order to initialize an environment
     * for playing a new game.
     */
    @EventTopicSubscriber(topic="NEWGAME")
    public void newGameEvent(String topic, Player[] players) {
        System.out.println("Model received event " + topic);
        newGame(players);
    }
    @EventTopicSubscriber(topic="BATTLE_ANS")
    public void onBattleAns(String topic, String answer) {
        System.out.println("Model received event " + topic);

        boolean correct = false;
        if(state.getBattle() != null){

            try{

                int theAnswer = Integer.parseInt(answer);
                System.out.println("ANSER: "+theAnswer+ ", Correct: "+state.getBattle().getQuestion().getAnswer());
                if(theAnswer == (int)(state.getBattle().getQuestion().getAnswer())){
                    correct = true;
                }
            }catch(NumberFormatException e){
                correct = false;
            }
            if(!correct)
                EventBus.publish(Events.ERROR.toString(), "Oh no! Your answer was incorrect");
        }

        rules.resolveBattle(correct);
        state.setBattle(null);
        EventBus.publish(Events.UPDATE.toString(), this);
    }

    /**
     * Handles if we should save a game.
     * @param fp the absolute filepath
     */
    @EventTopicSubscriber(topic="SAVE_GAME")
    public void saveGameEvent(String topic, String fp) {
        System.out.println("Model received event " + topic + " " + fp);
        Save save = new Save(state, fp+Constants.IO.file_ext_save);
        save.writeToFile();
    }
    /**
     * Handles if we should load a game
     * @param fp the absolute filepath
     */
    @EventTopicSubscriber(topic="LOAD_GAME")
    public void loadGameEvent(String topic, String fp) {
        System.out.println("Model received event " + topic + " " + fp);
        if(!state.isGameInProgress()){
            Load l = new Load();
            l.readFromFile(fp);
            if(l.getLoadedState() == null)
                return;

            state = l.getLoadedState();

            Player[] p = new Player[4];
            for(int i = 0; i < state.getNumOfPlayers(); i++)
                p[i] = state.getPlayer(i);

            this.players = p;
            this.playingfield = state.getPlayingField();
            //this.statistics = state.getStatistics(); not implemented
            this.rules = new model.Rules(this);

            timer = new TurnTimer();
            timer.start();

            EventBus.publish(Events.UPDATE.toString(), this);

        }else
            EventBus.publish(Events.ERROR.toString(), "Cannot load while the game is in progress!");
    }

    /**
     * Start a new turn. This involves performing several steps:
     *   - Update statistics for the current turn
     *   - Reset state variables that were changed by the current turn
     *   - Select a new player
     */
    @Override
    public void newTurn() {
        System.out.println("NewTurn() called");
        /*
         * Update the statistics for the current turn - that is, increase
         * the currents players turnCounter, and add the time he or she spent
         * on this turn, to this players total.
         */

        /*
         * Reset the various state variables, since we are about to
         * embark on a new turn.
         */

        state.setDieRolled(false);
        state.setSpotSelected(false);
        state.setMoveMade(false);

        /*
         * Select a new current player. Make sure that we do not select a
         * player out of bounds, and make sure that the player being selected
         * is still active in the game.
         */

        Integer nextPlayer = state.getCurrentPlayer() + 1;
        for(int i = 0; i < 4; i++) {

            if(nextPlayer > 3)
                nextPlayer = 0;

            if(players[nextPlayer] != null) {

                state.setCurrentPlayer(nextPlayer);
                break;
            }
            nextPlayer++;
        }
        timer.restart();
        audio.playSound(Constants.Sounds.newTurn);
    }

    /**
     * Starts a new game, complete with a new set of players, as well as a brand new State.
     * When all the initialization is finished, fire an UPDATE event in order to let the View
     * know it is time to redraw() itself to show the new data to the user.
     */
    private void newGame(Player[] players) {
        System.out.println("newGame() called");
        if(state.isGameInProgress())
            onEndGame(null, null);
        this.players = players;
        this.playingfield = new PlayingField(players);
        this.statistics = new Statistics(players);
        this.rules = new model.Rules(this);

        state = new State(players, playingfield, statistics);
        state.setGameInProgress(true);
        state.setCurrentPlayer(0);

        timer = new TurnTimer();
        timer.start();
        audio.playSound(Constants.Sounds.newGame);
        EventBus.publish(Events.UPDATE.toString(), this);

    }

    @Override
    public Statistics getStatistics() {
        return statistics;
    }

    @Override
    public PlayingField getPlayingField() {
        if(playingfield == null)
            System.out.println("PLAYINGFIELD IS NULL");
        return playingfield;
    }

    public AudioPlay getAudio(){
        return audio;
    }
}
